Skip to content

docs(adr): ADR-0100 — serve-side MCP default wiring; register the MCP tool-execution surface in the authz matrix (#3167)#3182

Closed
os-zhuang wants to merge 2 commits into
mainfrom
claude/mcp-server-default-wiring-ia5erh
Closed

docs(adr): ADR-0100 — serve-side MCP default wiring; register the MCP tool-execution surface in the authz matrix (#3167)#3182
os-zhuang wants to merge 2 commits into
mainfrom
claude/mcp-server-default-wiring-ia5erh

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Decision-issue response to #3167 (default wiring for the serve-side MCP server — identity admission first). #3167 makes decision #1 (identity admission) a hard gate: "nothing else in this issue proceeds before it." This PR lands the decision record and that gate, with an end-to-end proof.

Two findings that shaped the response

  1. The issue's premise is stale against main. decision: default wiring for the serve-side MCP server (@objectstack/mcp) in dev — identity admission first #3167 says MCPServerPlugin is "wired nowhere by default." Verified against main, MCP is already a default-on core capability: isMcpServerEnabled() defaults ON (packages/types/src/env.ts), packages/cli/src/commands/serve.ts pushes the mcp capability into os serve/os dev unless opted out, and the dispatcher route gate reads the same helper. The HTTP tool surface is also already fail-closed on identityhandleMcp returns 401 for a principal-less caller (no dev-admin fallback, no fall-open), and buildMcpBridge threads the caller ExecutionContext into callData(..., ec) on every verb (OAuth callers run at the ADR-0090 D10 ceiling∩user intersection).

  2. What was genuinely missing is exactly the gate. The MCP tool-execution surface had no row in the ADR-0096 D4 authz conformance matrix — so "whose ExecutionContext runs a tools/call" lived only in ADR-0096 prose, not as a checked, red-CI-on-regression property.

What this lands

  • docs/adr/0100-serve-side-mcp-default-wiring-identity-admission.md — the four decisions grounded in the verified state:
    • D1 identity admission — caller-principal (user) posture, fail-closed, registered as the gate.
    • D2 the dev/prod boundary is identity + the off-switch (OS_MCP_SERVER_ENABLED=false gates registration and route), not a code-level default flip (an opt-in-for-prod amendment is documented for ratification).
    • D3 HTTP-only default; stdio stays explicit opt-in (os dev owns the process; the stdio resource path carries the ADR-0096 E1 context-less read).
    • D4 bounded default exposure — fixed generic tool set over apiEnabled objects, sys_* fail-closed, actions only when ai.exposed; metadata-declared exposure is the future story.
  • authz-conformance.matrix.ts — two rows: enforced mcp-tool-exec-identity (fail-closed 401 gate + buildMcpBridge threading + OAuth scope gate; now carrying a real dogfood proof and marked high-risk) and experimental mcp-stdio-resource-read classifying the ADR-0096 E1 context-less stdio resource read (opt-in, never networked) with its admission requirement — registered honestly, not patched with a discouraged { isSystem: true } literal (ADR-0096 D2 retires those; the systemContext(reason) constructor doesn't exist yet).
  • authz-conformance.test.ts — three source probes pinning both halves of the HTTP admission and the stdio fall-open, so dropping the gate/threading or networking the stdio read trips CI (STALE / UNCLASSIFIED) instead of surfacing in an adversarial review later.
  • showcase-mcp-identity.dogfood.test.ts — the end-to-end proof: boots the showcase with MCPServerPlugin registered and asserts on the real HTTP stack that anonymous POST /mcp401 (not 404/501 — the surface is served and the service is registered; the denial is identity), an authenticated member clears the gate, and GET /mcp/skill200 (served default-on while tool-exec is gated).

Scope / safety

Docs + a private test package (@objectstack/dogfood) only — no runtime behavior change, no changeset. The identity path, OAuth admission (#2698), and ai.exposed + capability action gate are pre-existing and unchanged; this PR registers and proves them. (@objectstack/mcp added to the dogfood dev deps; lockfile updated.)

Validation

Validated locally: pnpm --filter @objectstack/dogfood test for authz-conformance + showcase-mcp-identity10/10 pass (7 conformance ratchet + 3 dogfood-boot). The three new probes each match their source anchor exactly once and are a clean bijection with the two rows' covers; the proof:-exists and high-risk assertions pass.

Open for ratification

  • D2: keep default-on-everywhere behind the off-switch (recommended), or adopt the NODE_ENV-aware opt-in-for-prod amendment.

Closes #3167 pending ratification of D2.

🤖 Generated with Claude Code

… tool-execution surface in the authz matrix (#3167)

#3167 is a decision issue whose #1 (identity admission) is the hard gate:
"nothing else proceeds before it." Two findings shaped the response:

1. The issue's premise — "MCPServerPlugin is wired nowhere by default" — is
   stale against main. MCP is already a default-on core capability
   (isMcpServerEnabled() default true + serve.ts capability push + the
   matching route gate), and the HTTP tool surface is already fail-closed on
   identity (handleMcp 401 for a principal-less caller; buildMcpBridge threads
   the caller ExecutionContext into callData on every verb; OAuth callers run
   at the ADR-0090 D10 ceiling∩user intersection).

2. What was genuinely missing is exactly the gate: the MCP tool-execution
   surface had NO row in the ADR-0096 D4 authz conformance matrix, so "whose
   ExecutionContext runs a tools/call" lived only in ADR-0096 prose, not as a
   checked, red-CI-on-regression property.

This lands:
- ADR-0100 recording the four decisions grounded in the verified state:
  D1 identity admission (caller-principal posture, registered as the gate),
  D2 dev/prod boundary is identity + off-switch not a default flip,
  D3 HTTP-only default with stdio kept explicit-opt-in,
  D4 bounded default exposure (apiEnabled + ai.exposed).
- Two authz-conformance rows: an enforced `mcp-tool-exec-identity` (fail-closed
  gate + bridge threading) and an experimental `mcp-stdio-resource-read`
  classifying the ADR-0096 E1 context-less stdio resource read (opt-in,
  never networked) with its admission requirement — registered honestly, not
  patched with a discouraged { isSystem: true } literal.
- Three source probes pinning both halves of the HTTP admission and the stdio
  fall-open, so dropping the gate/threading or networking the stdio read
  trips CI (STALE/UNCLASSIFIED) instead of being an adversarial-review finding.

Test-only + docs (packages/qa/dogfood is private) — no runtime behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155A3k1CMJQb44V4iwTLUcH
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 18, 2026 7:44am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): packages/qa.

2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authorization.mdx (via packages/qa)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…3167)

Upgrades the `mcp-tool-exec-identity` conformance row from cited-tests-only to
a real end-to-end boundary proof (ADR-0096 D4 requires one for an agent/
external-reachable surface). `showcase-mcp-identity.dogfood.test.ts` boots the
showcase with MCPServerPlugin registered (via the harness `extraPlugins` — the
verify harness boots the plugin set directly, not through the CLI capability
push) and asserts on the real in-process HTTP stack:
  - anonymous POST /mcp (tools/list) → 401 (fail-closed: not 404/501, so the
    surface IS served and the service IS registered — the denial is identity);
  - an authenticated member clears the gate (not 401);
  - GET /mcp/skill → 200 (surface served default-on while tool-exec is gated).

Wires it in as the row's `proof:` and adds the row to the test's HIGH_RISK set
(so the surface must carry a proof). Adds @objectstack/mcp to the private
@objectstack/dogfood dev deps (lockfile updated).

Validated locally: `pnpm --filter @objectstack/dogfood test` authz-conformance
+ showcase-mcp-identity → 10/10 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155A3k1CMJQb44V4iwTLUcH
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #3202, which already registered the MCP execution-surface identity admission in the ADR-0096 matrix (mcp-http-identity + mcp-stdio-authority rows + probes) for #3167. Closing as redundant — one of the probe keys is identical to #3202's, so this branch would double-classify the ledger. No separate decision ADR needed.


Generated by Claude Code

@os-zhuang os-zhuang closed this Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

decision: default wiring for the serve-side MCP server (@objectstack/mcp) in dev — identity admission first

2 participants